﻿#region Copyright

/* 
 * Will Strohl (will.strohl@gmail.com)
 * http://www.willstrohl.com
 * 
 * Copyright (c) 2012, Will Strohl
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are 
 * permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of 
 * conditions and the following disclaimer.
 * 
 * Redistributions in binary form must reproduce the above copyright notice, this list 
 * of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * 
 * Neither the name of Will Strohl, SugarSync Folder Provider, nor the names of its 
 * contributors may be used to endorse or promote products derived from this software 
 * without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 */

#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;

using DotNetNuke.Common;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Entities.Host;
using DotNetNuke.Services.FileSystem;
using DotNetNuke.Security;

using HigLabo.Net.SugarSync;

namespace WillStrohl.Providers.SugarSyncFolderProvider
{
    ///<summary>
    /// SugarSync Folder Provider
    ///</summary>
    public class SugarSyncFolderProvider : FolderProvider
    {
        #region Constants

        private const string AccessKeyId = "AccessKeyId";
        private const string PrivateAccessKey = "PrivateAccessKey";
        private const string Folder = "Folder";
        private const string Username = "Username";
        private const string Password = "Password";

        #endregion

        #region Private Variables

        private readonly string _encryptionKey = Host.GUID;
        private readonly PortalSecurity _portalSecurity = new PortalSecurity();

        private SugarSyncClient Client { get; set; }
        private string FolderName { get; set; }

        #endregion

        #region FolderProvider Methods

        /// <summary>
        /// Adds a new file to the specified folder.
        /// </summary>
        public override void AddFile(IFolderInfo folder, string fileName, Stream content)
        {
            Requires.NotNull("folder", folder);
            Requires.NotNullOrEmpty("fileName", fileName);
            Requires.NotNull("content", content);

            UpdateFile(folder, fileName, content);
        }

        /// <remarks>
        /// Amazon doesn't support folders. It only supports objects with content.
        /// We can differentiate files from folders by their Key. If the Key ends with "/" the object is a folder, otherwise it's a file.
        /// </remarks>
        public override void AddFolder(string folderPath, FolderMappingInfo folderMapping)
        {
            Requires.NotNull("folderPath", folderPath);
            Requires.NotNull("folderMapping", folderMapping);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                if (folderPath.Length > 0)
                {
                    var request = new PutObjectRequest()
                        .WithBucketName(bucket)
                        .WithKey(folderPath);

                    request.InputStream = new MemoryStream();

                    aws.PutObject(request);
                }
            }
        }

        /// <summary>
        /// Deletes the specified file.
        /// </summary>
        public override void DeleteFile(IFileInfo file)
        {
            Requires.NotNull("file", file);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(file.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                var request = new DeleteObjectRequest()
                    .WithBucketName(bucket)
                    .WithKey(file.Folder + file.FileName);

                aws.DeleteObject(request);
            }
        }

        /// <summary>
        /// Deletes the specified folder.
        /// </summary>
        public override void DeleteFolder(IFolderInfo folder)
        {
            Requires.NotNull("folder", folder);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(folder.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                if (!string.IsNullOrEmpty(folder.FolderPath))
                {
                    var request = new DeleteObjectRequest()
                        .WithBucketName(bucket)
                        .WithKey(folder.FolderPath);

                    aws.DeleteObject(request);
                }
            }
        }

        /// <summary>
        /// Checks the existence of the specified file.
        /// </summary>
        public override bool ExistsFile(IFolderInfo folder, string fileName)
        {
            Requires.NotNull("folder", folder);
            Requires.NotNullOrEmpty("fileName", fileName);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(folder.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                try
                {
                    var request = new GetObjectMetadataRequest()
                        .WithBucketName(bucket)
                        .WithKey(folder.FolderPath + fileName);

                    using (aws.GetObjectMetadata(request))
                    {
                        return true;
                    }
                }
                catch (AmazonS3Exception ex)
                {
                    if (ex.StatusCode == HttpStatusCode.NotFound)
                    {
                        return false;
                    }

                    throw;
                }
            }
        }

        /// <summary>
        /// Checks the existence of the specified folder.
        /// </summary>
        public override bool ExistsFolder(string folderPath, FolderMappingInfo folderMapping)
        {
            Requires.NotNull("folderPath", folderPath);
            Requires.NotNull("folderMapping", folderMapping);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                if (folderPath.Length == 0) return true;

                var request = new GetObjectMetadataRequest()
                    .WithBucketName(bucket)
                    .WithKey(folderPath);

                try
                {
                    using (aws.GetObjectMetadata(request))
                    {
                        return true;
                    }
                }
                catch (AmazonS3Exception ex)
                {
                    if (ex.StatusCode == HttpStatusCode.NotFound)
                    {
                        // The folder doesn't exists physically but there could be files with key = folderPath + fileName
                        var listRequest = new ListObjectsRequest()
                            .WithBucketName(bucket)
                            .WithPrefix(folderPath);

                        using (var response = aws.ListObjects(listRequest))
                        {
                            return response.S3Objects.Count > 0;
                        }
                    }

                    throw;
                }
            }
        }

        /// <summary>
        /// Gets the content of the specified file.
        /// </summary>
        public override byte[] GetFile(IFileInfo file)
        {
            Requires.NotNull("file", file);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(file.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                var request = new GetObjectRequest()
                    .WithBucketName(bucket)
                    .WithKey(file.Folder + file.FileName);

                using (var response = aws.GetObject(request))
                {
                    var buffer = new byte[16 * 1024];
                    
                    using (var ms = new MemoryStream())
                    {
                        int read;
                        
                        while ((read = response.ResponseStream.Read(buffer, 0, buffer.Length)) > 0)
                        {
                            ms.Write(buffer, 0, read);
                        }
                        
                        return ms.ToArray();
                    }
                }
            }
        }

        /// <remarks>
        /// Amazon doesn't support file attributes.
        /// </remarks>
        public override FileAttributes? GetFileAttributes(IFileInfo file)
        {
            return null;
        }

        /// <summary>
        /// Gets the file length.
        /// </summary>
        public override long GetFileLength(IFileInfo file)
        {
            Requires.NotNull("file", file);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(file.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                var request = new GetObjectMetadataRequest()
                    .WithBucketName(bucket)
                    .WithKey(file.Folder + file.FileName);

                using (var response = aws.GetObjectMetadata(request))
                {
                    return long.Parse(response.Headers["Content-Length"]);
                }
            }
        }

        /// <summary>
        /// Gets a file Stream of the specified file.
        /// </summary>
        public override Stream GetFileStream(IFolderInfo folder, string fileName)
        {
            Requires.NotNull("folder", folder);
            Requires.NotNullOrEmpty("fileName", fileName);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(folder.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                var request = new GetObjectRequest()
                    .WithBucketName(bucket)
                    .WithKey(folder.FolderPath + fileName);

                var response = aws.GetObject(request);

                return new AmazonStream(response);
            }
        }

        /// <summary>
        /// Gets the direct Url to the file.
        /// </summary>
        public override string GetFileUrl(IFileInfo file)
        {
            Requires.NotNull("file", file);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(file.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                var request = new GetPreSignedUrlRequest()
                    .WithProtocol(protocol == "HTTP" ? Amazon.S3.Model.Protocol.HTTP : Amazon.S3.Model.Protocol.HTTPS)
                    .WithBucketName(bucket)
                    .WithKey(file.Folder + file.FileName)
                    .WithExpires(new DateTime(2038, 1, 19)); // Max expiration date supported by Amazon
                
                return aws.GetPreSignedURL(request);
            }
        }

        /// <summary>
        /// Gets the list of file names contained in the specified folder.
        /// </summary>
        public override string[] GetFiles(IFolderInfo folder)
        {
            Requires.NotNull("folder", folder);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(folder.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                var request = new ListObjectsRequest()
                    .WithBucketName(bucket)
                    .WithPrefix(folder.FolderPath)
                    .WithMarker(folder.FolderPath)
                    .WithDelimiter("/");

                using (var response = aws.ListObjects(request))
                {
                    return response.S3Objects
                        .Select(obj => Path.GetFileName(obj.Key))
                        .ToArray();
                }
            }
        }

        /// <summary>
        /// Gets the URL of the image to display in FileManager tree.
        /// </summary>
        public override string GetImageUrl()
        {
            return Globals.ResolveUrl("~/Providers/FolderProviders/WillStrohl.SugarSync/images/icon_sugarsync.png");
        }

        /// <summary>
        /// Gets the time when the specified file was last modified.
        /// </summary>
        public override DateTime GetLastModificationTime(IFileInfo file)
        {
            Requires.NotNull("file", file);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(file.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                var request = new GetObjectMetadataRequest()
                    .WithBucketName(bucket)
                    .WithKey(file.Folder + file.FileName);

                using (var response = aws.GetObjectMetadata(request))
                {
                    DateTime lastModificationTime;

                    DateTime.TryParse(response.Headers["Last-Modified"], out lastModificationTime);

                    return lastModificationTime;
                }
            }
        }

        /// <summary>
        /// Gets the list of subfolders for the specified folder.
        /// </summary>
        public override IEnumerable<string> GetSubFolders(string folderPath, FolderMappingInfo folderMapping)
        {
            Requires.NotNull("folderPath", folderPath);
            Requires.NotNull("folderMapping", folderMapping);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                var request = new ListObjectsRequest()
                    .WithBucketName(bucket)
                    .WithPrefix(folderPath)
                    .WithMarker(folderPath)
                    .WithDelimiter("/");

                using (var response = aws.ListObjects(request))
                {
                    return response.CommonPrefixes;
                }
            }
        }

        /// <summary>
        /// Indicates if the specified file is synchronized.
        /// </summary>
        /// <remarks>
        /// For now, it returns false always until we find a better way to check if the file is synchronized.
        /// </remarks>
        public override bool IsInSync(IFileInfo file)
        {
            return false;
        }

        /// <summary>
        /// Renames the specified file using the new filename.
        /// </summary>
        public override void RenameFile(IFileInfo file, string newFileName)
        {
            Requires.NotNull("file", file);
            Requires.NotNullOrEmpty("newFileName", newFileName);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(file.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                var copyRequest = new CopyObjectRequest()
                    .WithSourceBucket(bucket)
                    .WithSourceKey(file.Folder + file.FileName)
                    .WithDestinationBucket(bucket)
                    .WithDestinationKey(file.Folder + newFileName);

                aws.CopyObject(copyRequest);

                var deleteRequest = new DeleteObjectRequest()
                    .WithBucketName(bucket)
                    .WithKey(file.Folder + file.FileName);

                aws.DeleteObject(deleteRequest);
            }
        }

        /// <summary>
        /// Renames the specified folder using the new foldername.
        /// </summary>
        public override void RenameFolder(IFolderInfo folder, string newFolderName)
        {
            Requires.NotNull("folder", folder);
            Requires.NotNullOrEmpty("newFolderName", newFolderName);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(folder.FolderMappingID);

            CheckSettings(folderMapping);

            var accessKeyID = GetEncryptedSetting(folderMapping, AccessKeyId);
            var secretAccessKey = GetEncryptedSetting(folderMapping, SecretAccessKey);
            var bucket = GetSetting(folderMapping, Bucket);
            var protocol = GetSetting(folderMapping, Protocol);
            var config = GetAmazonConfig(protocol);

            using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            {
                if (!string.IsNullOrEmpty(folder.FolderPath))
                {
                    var newFolderPath = PathUtils.Instance.FormatFolderPath(
                        folder.FolderPath.Substring(0, folder.FolderPath.LastIndexOf(folder.FolderName)) + newFolderName);

                    var listObjectsRequest = new ListObjectsRequest()
                        .WithBucketName(bucket)
                        .WithPrefix(folder.FolderPath);

                    var listObjectsResponse = aws.ListObjects(listObjectsRequest);

                    foreach (var subObject in listObjectsResponse.S3Objects)
                    {
                        var newDestinationKey = (subObject.Key == folder.FolderPath) ? newFolderPath : newFolderPath + subObject.Key.Substring(folder.FolderPath.Length);

                        var copyRequest = new CopyObjectRequest()
                            .WithSourceBucket(bucket)
                            .WithSourceKey(subObject.Key)
                            .WithDestinationBucket(bucket)
                            .WithDestinationKey(newDestinationKey);

                        aws.CopyObject(copyRequest);

                        var deleteRequest = new DeleteObjectRequest()
                            .WithBucketName(bucket)
                            .WithKey(subObject.Key);

                        aws.DeleteObject(deleteRequest);
                    }
                }
            }
        }

        /// <remarks>
        /// No implementation needed because this provider doesn't support FileAttributes.
        /// </remarks>
        public override void SetFileAttributes(IFileInfo file, FileAttributes fileAttributes)
        {
        }

        /// <remarks>
        /// Amazon doesn't support file attributes.
        /// </remarks>
        public override bool SupportsFileAttributes()
        {
            return false;
        }

        /// <summary>
        /// Updates the content of the specified file.
        /// </summary>
        public override void UpdateFile(IFolderInfo folder, string fileName, Stream content)
        {
            Requires.NotNull("folder", folder);
            Requires.NotNullOrEmpty("fileName", fileName);
            Requires.NotNull("content", content);

            var folderMapping = FolderMappingController.Instance.GetFolderMapping(folder.FolderMappingID);

            CheckSettings(folderMapping);

            LoadClient(folder.FolderMappingID);

            var cmdFile = new UpdateFileCommand();
            cmdFile.DisplayName = fileName;

            Client.UpdateFile(cmdFile);

            var cmdFileData = new PutFileDataCommand();
            cmdFileData.FileName = fileName;
            cmdFileData.FilePath = "";
            cmdFileData.FolderPath = folder.FolderPath;
            cmdFileData.CreatePostData(content);

            Client.PutFileData(cmdFileData);

            //using (var aws = AWSClientFactory.CreateAmazonS3Client(accessKeyID, secretAccessKey, config))
            //{
            //    var request = new PutObjectRequest()
            //        .WithBucketName(bucket)
            //        .WithKey(folder.FolderPath + fileName);

            //    request.AutoCloseStream = false;
            //    request.InputStream = content;

            //    aws.PutObject(request);
            //}
        }

        #endregion

        #region Private Methods

        private void LoadClient(int folderMappingId)
        {
            var folderMappingController = FolderMappingController.Instance;
            var folderMapping = folderMappingController.GetFolderMapping(folderMappingId);

            var accessKeyId = folderMapping.FolderMappingSettings[AccessKeyId].ToString();
            var privateAccessKey = folderMapping.FolderMappingSettings[PrivateAccessKey].ToString();
            //var folderName = folderMapping.FolderMappingSettings[Folder].ToString();
            var username = folderMapping.FolderMappingSettings[Username].ToString();
            var password = folderMapping.FolderMappingSettings[Password].ToString();

            Client = new SugarSyncClient(username, password, accessKeyId, privateAccessKey);
            Client.Authenticate();
        }

        private string GetEncryptedSetting(FolderMappingInfo folderMapping, string settingName)
        {
            return _portalSecurity.Decrypt(_encryptionKey, folderMapping.FolderMappingSettings[settingName].ToString());
        }

        private static string GetSetting(FolderMappingInfo folderMapping, string settingName)
        {
            return folderMapping.FolderMappingSettings[settingName].ToString();
        }

        private static void CheckSettings(FolderMappingInfo folderMapping)
        {
            var settings = folderMapping.FolderMappingSettings;

            if (string.IsNullOrEmpty((string)settings[AccessKeyId]) ||
                string.IsNullOrEmpty((string)settings[SecretAccessKey]) ||
                string.IsNullOrEmpty((string)settings[Bucket]) ||
                string.IsNullOrEmpty((string)settings[Protocol]))
            {
                throw new Exception("Settings cannot be found.");
            }
        }

        #endregion
    }
}
